const ERROR = 'Error';
const INFO = 'Info';

var Profiler = (function() {
	const Cc = Components.classes;
	const Ci = Components.interfaces;
	const Cu = Components.utils;
	const EXTENSION_ID = 'toolkit@labs.nuttycoder.com';
	const PROFILE_FILE_NAME = 'memory-profiler.trace.js';
	const CACHE_FILE_NAME = 'snapshots.tmp';
	const MAX_APP_NAME_LEN = 20;

	var EXT_MANAGER = Cc['@mozilla.org/extensions/manager;1'].getService(Components.interfaces.nsIExtensionManager);
	var LOC = EXT_MANAGER.getInstallLocation(EXTENSION_ID);
	var CACHE_FILE = LOC.getItemLocation(EXTENSION_ID);
	CACHE_FILE.append(CACHE_FILE_NAME);
	var PROFILE_FILE = LOC.getItemLocation(EXTENSION_ID);
	PROFILE_FILE.append('content');
	PROFILE_FILE.append('memory-profiler-0.2');
	PROFILE_FILE.append('js');
	PROFILE_FILE.append(PROFILE_FILE_NAME);
	var PROFILE_CODE = readFile(PROFILE_FILE);
	var COM = getBinaryComponent();

	if (!PROFILE_FILE) {
		// TODO: no profile script;
		return null;
	}
	if (!COM) {
		// NOTE:
		log('Required binary component not found! One may not be available ' + 'for your OS and Firefox version.', ERROR);
		return null;
	}

	var snapshots = [];
	var apps = getAppData();
	var subscription = [];

	return {
		getSnapshots: function() {
			return snapshots;
		},
		getApps: function() {
			apps = getAppData();
			return apps;
		},
		reloadApps: function() {
			apps = getAppData();
			return getApps();
		},
		profile: function(id) {
			doProfile(apps[id]);
			return snapshots;
		},
		sub: function(subject, func, context) {
			if (!subscription[subject]) {
				subscription[subject] = [];
			}
			subscription[subject].push({
				func: func,
				context: context
			});
		}
	};
	function getAppData() {
		var apps = [];
		var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
		var enumerator = wm.getEnumerator("navigator:browser");
		while (enumerator.hasMoreElements()) {
			var win = enumerator.getNext();
			if (win.gBrowser) {
				var browser = win.gBrowser;
				for (var i = 0; i < browser.browsers.length; i++) {
					var page = browser.browsers[i];
					var name = page.contentTitle || page.currentURI.spec;
					apps.push({
						id: apps.length,
						browser: page,
						name: name,
						href: page.contentWindow.location.href
					});
				}
			}
		}
		return apps;
	}
	function recursivelyGetIframes(document) {
		var iframes = [];
		var subframes = getIframes(document);
		subframes.forEach(
		function(iframe) {
			iframes.push(iframe.contentWindow.wrappedJSObject);
			var children = recursivelyGetIframes(iframe.contentDocument);
			iframes = iframes.concat(children);
		});
		return iframes;
	}
	function htmlCollectionToArray(coll) {
		var array = [];
		for (var i = 0; i < coll.length; i++) {
			array.push(coll[i]);
		}
		return array;
	}
	function getIframes(document) {
		return htmlCollectionToArray(document.getElementsByTagName("iframe"));
	}
	function getBinaryComponent() {
		try {
			var factory = Cc['@labs.mozilla.com/jetpackdi;1'].createInstance(Ci.nsIJetpack);
			return factory.get();
		} catch(e) {
			return null;
		}
	}
	function readFile(file) {
		return FileIO.read(file, 'utf-8');
	}
	function writeFile(file, data) {
		FileIO.write(file, JSON.stringify(data), null, 'utf-8');
	}
	function doProfile(appInfo) {
		try {
			// NOTE:
			if (appInfo) {
				var browser = appInfo.browser;
				var iframes = recursivelyGetIframes(browser.contentDocument);
				var windowsToProfile = [browser.contentWindow.wrappedJSObject];
				windowsToProfile = windowsToProfile.concat(iframes);
			} else {
				windowsToProfiler = null;
			}

			// NOTE: force GC;
			Components.utils.forceGC();

			writeFile(CACHE_FILE, snapshots);
			snapshots = null;

			// NOTE: profiling;
			var startTime = new Date();
			var result = COM.profileMemory(PROFILE_CODE, PROFILE_FILE, 1, windowsToProfile);
			var totalTime = (new Date()) - startTime;
			log('profile success');
			log(totalTime + ' ms were spent in memory profiling.');

			// NOTE: restore snapshots;
			snapshots = Snapshot.restore(readFile(CACHE_FILE));

			// NOTE: parse profiling results;
			result = JSON.parse(result);
			if (result.success) {
				if (appInfo) {
					snapshots.push(new Snapshot(snapshots.length, appInfo.id, appInfo.name.slice(0, MAX_APP_NAME_LEN) + ' @ ' + startTime.toTimeString(), result.data));
				} else {
					snapshots.push(new Snapshot(snapshots.length, 0, 'Cross All @ ' + startTime.toTimeString(), result.data));
				}
				var c = 0;
				for (var p in result.data) {
					c++;
				}
				Ui.log(c + ' objects found.');
			} else {
				Ui.log('An error occurred while profiling.', ERROR);
				Ui.log(result.traceback, ERROR);
				Ui.log(result.error, ERROR);
			}
		} catch(ex) {
			Ui.log(ex, ERROR);
		}
	}
	function pub(subject, data) {
		$(subscription[subject]).each(function(index, elem) {
			elem.func.call(elem.context, data);
		});
	}
	function log(msg, type) {
		pub('log', {
			message: msg,
			type: type
		});
	}
})();

var Ui = (function() {
	const ERROR = 'Error';
	const INFO = 'Info';
	const LENGTH_TO_CONFIRM = 20;
	const MAX_TAB_NAME_LEN = 20;

	var $$ = {
		BTNS: 'input:button',
		BTN_PROFILE: '#profile',
		BTN_SEARCH: '#search',
		ELEM_OBJ_ID: '.object-id',
		INPUT_PROP: '#prop',
		SEL_APPS: '#apps',
		RAT_CROSS: '#header input[type=radio][name=cross]:checked',
		SEL_SNAPSHOTS: '#snapshots',
		SEL_SNAPSHOT_1: '#snapshot1',
		SEL_SNAPSHOT_2: '#snapshot2'
	};

	var hieracheyData = {};
	var subscription = {};
	var activeSnapshot = null;
	var snapshots = [];

	return {
		init: _init,
		sub: _sub,
		setApps: _setApps,
		setSnapshots: _setSnapshots,
		addSnapshot: _addSnapshot,
		log: _log
	};
	function _setActiveSnapshot(s) {
		activeSnapshot = s;
	}
	function resetHierachey() {
		hieracheyData = [];
		$('#hierachey').empty();
	}
	function _sub(subject, func, context) {
		if (!subscription[subject]) {
			subscription[subject] = [];
		}
		subscription[subject].push({
			func: func,
			context: context
		});
	}
	function pub(subject, data) {
		$(subscription[subject]).each(function(index, elem) {
			elem.func.call(elem.context, data);
		});
	}
	function _init() {
		$($$.BTNS).button();
		$($$.BTN_PROFILE).click(function(evt) {
			pub('profile', {
				status: getStatus()
			});
		});
		$($$.BTN_SEARCH).click(function(evt) {
			/*
			pub('search', {
				status: getStatus()
			});
			*/
			try {
				addColumnToHierachey(searchObjectsByProperty($($$.INPUT_PROP).val()));
			} catch(ex) {
				_log(ex, ERROR);
			}
		});
		$($$.ELEM_OBJ_ID).live('click', function(evt) {
			try {
				showObjectDetails($(this).text());
			} catch(ex) {
				_log(ex, ERROR);
			}
		});
		$($$.SEL_SNAPSHOTS).change(function(evt) {
			activeSnapshot = evt.currentTarget.value;
		});
		$('a[href=#expose]').live('click', function(evt) {
			try {
				$(this).parent().find('a').toggleClass('hide');
				$(this).parent().next().toggleClass('hide');
				var id = $(this).attr('id');
				if (!hieracheyData[id]) {
					hieracheyData[id] = searchObjectsByChildren(id);
					addColumnToHierachey(hieracheyData[id], id);
				}
			} catch(ex) {
				_log(ex, ERROR);
			}
			return false;
		});
		$('a[href=#object-detail]').live('click', function(evt) {
			try {
				showObjectDetails($(this).attr('id'));
			} catch(ex) {
				_log(ex, ERROR);
			}
			return false;
		});
		$('#object-detail-tabs').tabs({
			show: function(evt, ui) {
				var id = $(ui.tab).find('span').text();
				$('#hierachey h3 a.active').removeClass('active');
				$('#hierachey h3#' + id + ' a:last').addClass('active');
			}
		});
		$('#object-detail-tabs li.ui-tabs-selected').live('dblclick', function(evt) {
			var ul = $(this).parent();
			var lis = ul.children();
			var index = lis.index($(this));
			$('#object-detail-tabs').tabs('remove', index);
		});
		$('#compare').click(function(evt) {
			activeSnapshot = $('#snapshot2').val();
			compareSnapshots(snapshots[$('#snapshot1').val()].data, snapshots[$('#snapshot2').val()].data, $('#identifier').val());
		});
		$('#refreshApp').click(function(evt) {
			Ui.setApps(Profiler.getApps());
		});
		$('#clearLog').click(function(evt) {
			$('#log').empty();
		});
	}
	function compareSnapshots(snapshot1, snapshot2, id) {
		try {
			var objs = [];
			var s1 = {},
			s2 = {};
			id = id || '_jsxid';
			for (var p in snapshot1) {
				if (snapshot1[p].properties && snapshot1[p].properties[id]) {
					s1[snapshot1[p].properties[id]] = snapshot1[p];
				}
			}
			for (p in snapshot2) {
				if (snapshot2[p].properties && snapshot2[p].properties[id]) {
					s2[snapshot2[p].properties[id]] = snapshot2[p];
				}
			}
			for (p in s2) {
				if (p in s1) {} else {
					objs.push(s2[p]);
				}
			}
			addColumnToHierachey(objs);
		} catch(ex) {
			_log(ex, ERROR);
		}
	}
	function showObjectDetails(id) {
		if (!$('#object-details-tab-' + id).size()) {
			var obj = searchById(id);
			var html = getObjectDescription(obj);
			$(html).appendTo('#object-detail-tabs');
			$('#object-detail-tabs').tabs('add', '#object-details-tab-' + id, id, 0);
			$('#object-detail-tabs').tabs('select', 0);
		} else {
			var li = $('a[href=#object-details-tab-' + id + ']').parent();
			var lis = li.parent().children();
			var index = lis.index(li);
			$('#object-detail-tabs').tabs('select', index);
		}
	}
	function searchObjectsByProperty(prop) {
		if (!activeSnapshot) {
			activeSnapshot = $($$.SEL_SNAPSHOTS).val();
		}
		return snapshots[activeSnapshot].searchByProperty(prop);
	}
	function showSnapshots(sel, snapshots) {
		sel.empty();
		$(snapshots).each(function(index, elem) {
			var option = $('<option value="' + elem.id + '">' + elem.title + '</option>');
			sel.append(option);
		});
	}
	function _setSnapshots(s) {
		snapshots = s;
		showSnapshots($($$.SEL_SNAPSHOTS), snapshots);
		showSnapshots($($$.SEL_SNAPSHOT_1), snapshots);
		showSnapshots($($$.SEL_SNAPSHOT_2), snapshots);
	};
	function _addSnapshot(snapshot) {
		snapshots.push(snapshot);
		var option = $('<option value="' + snapshot.id + '">' + snapshot.title + '</option>');
		$($$.SEL_SNAPSHOTS).append(option);
	};
	function _addApp(app) {};
	function _setApps(apps) {
		$($$.SEL_APPS).empty();
		$(apps).each(function(index, elem) {
			var option = $('<option value="' + index + '">' + elem.name.slice(0, MAX_TAB_NAME_LEN) + '</option>');
			$($$.SEL_APPS).append(option);
		});
	}
	function getStatus() {
		return {
			cross: $($$.RAT_CROSS).val(),
			app: $($$.SEL_APPS).val(),
			snapshot: $($$.SEL_SNAPSHOTS).val(),
			prop: $($$.INPUT_PROP).val()
		};
	}
	function searchById(id) {
		var profile = snapshots[activeSnapshot].data;
		if (profile[id]) {
			_log('Search by id "' + id + '": 1 object found.');
		}
		return profile[id];
	}
	function searchObjectsByChildren(child) {
		try {
			var profile = snapshots[activeSnapshot].data;
			child = parseInt(child);
			var objs = [];
			for (var p in profile) {
				if (profile[p].children && profile[p].children.indexOf(child) > - 1) {
					objs.push(profile[p]);
				}
			}
			_log('Search by children "' + child + '": ' + objs.length + ' objects found.');
			return objs;
		} catch(ex) {
			_log(ex, ERROR);
		}
	}
	function getObjectName(obj) {
		if (!obj.allProps) {
			obj.allProps = getAllProperties(obj.prototype);
		}
		if (!obj.jsxclass && obj.properties) {
			var __jsxclass = obj.properties['__jsxclass__'] || obj.allProps['__jsxclass'] || obj.allProps['__jsxclass__'];
			if (__jsxclass) {
				__jsxclass = searchById(__jsxclass);
				if (__jsxclass && __jsxclass.properties) {
					var jsxclass = searchById(__jsxclass.properties['jsxclass']);
					if (jsxclass) {
						obj.jsxclass = jsxclass.properties._name || jsxclass.properties.JM;
					}
				}
			}
		}
		if (!obj.jsxname) {
			obj.jsxname = obj.properties.jsxname;
			obj.jsxid = obj.properties._jsxid;
		}
		var name = obj.id + ' ' + obj.nativeClass;
		if (obj.jsxname || obj.jsxid || obj.jsxclass) {
			name += ' (';
			if (obj.jsxclass) {
				name += '@' + obj.jsxclass;
			}
			if (obj.jsxname || obj.jsxid) {
				name += obj.jsxid + '/' + obj.jsxname;
			}
			name += ')';
		}
		return name;
	}
	function getAllProperties(id) {
		var props = {};
		var obj = searchById(id);
		if (obj) {
			if (obj.prototype) {
				var protoProps = getAllProperties(obj.prototype);
				for (p in protoProps) {
					props[p] = protoProps[p];
				}
			}
			if (obj.properties) {
				for (var p in obj.properties) {
					props[p] = obj.properties[p];
				}
			}
		}
		return props;
	}
	function wrapObjectIdHtml(id) {
		if (typeof(id) == 'object') {
			for (var html = '', i = 0; i < id.length; i++) {
				html += '<span class="object-id">' + id[i] + '</span> '
			}
			return html;
		} else {
			return '<span class="object-id">' + id + '</span> ';
		}
	}
	function ifObjectId(str) {
		return str in snapshots[activeSnapshot].data;
	}
	function getObjectDescription(obj) {
		if (typeof(obj) == 'object') {
			var desc = ['<h4>Basic Info</h4>', 'nativeClass: ', obj.nativeClass, '<br />', 'parent: ', wrapObjectIdHtml(obj.parent), '<br />', 'prototype: ', wrapObjectIdHtml(obj.prototype), '<br />', 'children: ', wrapObjectIdHtml(obj.children)];
			if (obj.properties) {
				desc.push('<h4>Properties</h4>');
				for (var p in obj.properties) {
					desc = desc.concat([p, ' => ', ifObjectId(obj.properties[p]) ? wrapObjectIdHtml(obj.properties[p]) : obj.properties[p] + '', '<br />']);
				}
			}
			if (!obj.allProps) {
				obj.allProps = getAllProperties(obj.prototype);
			}
			desc.push('<h4>Properties in prototypes</h4>');
			for (p in obj.allProps) {
				// NOTE: property is in prorotype but not this object;
				if (!(p in obj.properties)) {
					desc = desc.concat([p, ' => ', ifObjectId(obj.allProps[p]) ? wrapObjectIdHtml(obj.allProps[p]) : obj.allProps[p] + '', '<br />']);
				}
			}
			if (obj.nativeClass == 'Function') {
				desc = desc.concat(['<h4>Function Info</h4>', 'name: ', obj.name, '<br />', 'filename: ', obj.filename, '<br />', 'lineStart: ', obj.lineStart, '<br />', 'lineEnd: ', obj.lineEnd]);
			}

			return '<div id="object-details-tab-' + obj.id + '" class="object-details">' + desc.join('') + '</div>';
		} else if (typeof(obj) == 'string') {
			return '<h4>No detailed info</h4>';
		}
	}
	function addColumnToHierachey(objs, parent) {
		if (!parent) {
			resetHierachey();
			var ul = $('ul#hierachey');
		} else {
			var ul = $('ul#children-' + parent);
		}
		if (objs.length > LENGTH_TO_CONFIRM) {
			if (!window.confirm('Adding ' + objs.length + ' objects to the hierachey. Large numbers of objects will make profiler very slow, continue?')) {
				return;
			}
		}
		$(objs).each(function(index, obj) {
			var li = $('<li><h3 id="' + obj.id + '"><a href="#expose" id="' + obj.id + '" class="icon">+</a><a href="#expose" id="' + obj.id + '" class="hide icon">-</a> <a href="#object-detail" id="' + obj.id + '">' + getObjectName(obj) + '</a></h3><ul id="children-' + obj.id + '" class="ref-list hide"></ul></li>');
			ul.append(li);
		});
	}
	function _log(message, level) {
		level = level || INFO;
		if (typeof(message) == 'object') {
			message = JSON.stringify(message);
		}
		var elem = $('<p></p>');
		elem.hide();
		elem.html(['[', level, '] ', message].join(''));
		$('#log').append(elem);
		elem.slideDown();
		if (level == ERROR) {
			alert(message);
		}
	}
})();

var Snapshot = function(id, app, title, data) {
	this.id = id;
	this.app = app;
	this.title = title;
	this.data = data;
};
Snapshot.restore = function(str) {
	var arr = JSON.parse(str);
	var snapshots = [];
	for (var i = 0; i < arr.length; i ++) {
		snapshots.push(new Snapshot(arr[i].id, arr[i].app, arr[i].title, arr[i].data));
	}
	return snapshots;
};
Snapshot.prototype = {
	searchByProperty: function(prop) {
		try {
			var objs = [];
			var profile = this.data;

			for (var p in profile) {
				if (profile[p].properties && (prop in profile[p].properties)) {
					objs.push(profile[p]);
				}
			}
			Ui.log('Search by property "' + prop + '": ' + objs.length + ' objects found.');
			return objs;
		} catch(ex) {
			Ui.log(ex, ERROR);
		}
	}
};

function onReady() {
	function log(data) {
		Ui.log(data.message, data.level);
	}
	try {
		Ui.init();
		Ui.setApps(Profiler.getApps());
		Ui.sub('profile', function(data) {
			// TODO:
			var result = Profiler.profile(data.status.cross == 'true' ? - 1: data.status.app);
			Ui.setSnapshots(Profiler.getSnapshots());
		});
		Profiler.sub('log', log);
	} catch(ex) {
		Ui.log(ex, ERROR);
	}
}
$(window).ready(onReady);
